#include "bzbcrypto/crypto.h"
#include "bzbutil/platform.h"
#include "bzbstring/stringutils.h"
#include <exception>

#ifdef BZBCRYPTO_UNSTABLE

#define GCRYPT_NO_DEPRECATED
#include <gcrypt.h>
#include <memory>

bool gcry_initialized = false;

#else
#if defined(WINDOWS)

#define WINDOWS_LEAN_AND_MEAN 1
#include <windows.h>
#include <wincrypt.h>

#elif defined(LINUX)

#include <unistd.h>
#include <sys/random.h>
#include <syscall.h>

#endif
#endif


int bzbcrypto::get_secure_random() {

#ifdef BZBCRYPTO_UNSTABLE

	if(!gcry_initialized) {
		init_gcrypt();
	}

	int res;
	gcry_randomize((unsigned char*) &res, sizeof(int), GCRY_STRONG_RANDOM);
	return res;

#else

#if defined(WINDOWS)

	HCRYPTPROV hProvider;

    if(!CryptAcquireContext(&hProvider, nullptr, nullptr, PROV_RSA_FULL, 0)) {
      if(GetLastError() == (DWORD) NTE_BAD_KEYSET) {
        if(!CryptAcquireContext(&hProvider, nullptr, nullptr,
        						PROV_RSA_FULL, CRYPT_NEWKEYSET)) {
        	
        	throw MException(M_ERROR::NO_CRYPTO);
        }
      }
    }

    int res;
    CryptGenRandom(hProvider, 4, (BYTE*) &res);
    return res;

#elif defined(LINUX) && defined(SYS_getrandom)

	int res;
	int err = getrandom(&res, 4, 0);
	if(err < 4) {
		bzbutil::throw_sys_error();
	}
	return res;

#else
#error Unsupported function
#endif
#endif

}


#ifdef BZBCRYPTO_UNSTABLE


void bzbcrypto::init_gcrypt() {

	if (!gcry_check_version(GCRYPT_VERSION)) {
		throw std::runtime_error("ERROR: libgcrypt: gcry_check_version failed");
	}

	// Initialize secure memory
	// gcry_control(GCRYCTL_INIT_SECMEM, 16384, 0);
	// gcry_control(GCRYCTL_RESUME_SECMEM_WARN);
	gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
	if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
		throw std::runtime_error("ERROR: libgcrypt: libgcrypt initialization failed");
	}

	gcry_initialized = true;
}


std::string bzbcrypto::digest_SHA256(std::string plaintext) {

	if(!plaintext.size()) {
		throw std::runtime_error("ERROR: bzbcrypto: Cannot digest an empty string");
	}

	if(!gcry_initialized) {
		init_gcrypt();
	}

	std::unique_ptr<unsigned char> buffer(new unsigned char[plaintext.size()]);
	memcpy((void*) buffer.get(), (void*) &plaintext[0], plaintext.size() + 1);

	size_t digest_size = gcry_md_get_algo_dlen(GCRY_MD_SHA256);
	std::unique_ptr<unsigned char> digest(new unsigned char[digest_size]);
	
	gcry_md_hash_buffer(GCRY_MD_SHA256, digest.get(), buffer.get(), plaintext.size());

	std::string res;
	for(size_t i = 0; i < digest_size; i++) {
		res.append(bzbstring::hex(digest.get()[i]));
	}

	return res;
}


std::string bzbcrypto::digest_SHA256(std::vector<unsigned char> data) {

	if(!data.size()) {
		throw std::runtime_error("ERROR: digest_SHA256: Cannot digest an empty vector");
	}

	if(!gcry_initialized) {
		init_gcrypt();
	}

	std::unique_ptr<unsigned char> buffer(new unsigned char[data.size()]);
	memcpy((void*) buffer.get(), (void*) &data[0], data.size() + 1);

	size_t digest_size = gcry_md_get_algo_dlen(GCRY_MD_SHA256);
	std::unique_ptr<unsigned char> digest(new unsigned char[digest_size]);
	
	gcry_md_hash_buffer(GCRY_MD_SHA256, digest.get(), buffer.get(), data.size());

	std::string res;
	for(size_t i = 0; i < digest_size; i++) {
		res.append(bzbstring::hex(digest.get()[i]));
	}

	return res;
}


std::string bzbcrypto::digest_SHA512(std::string plaintext) {

	if(!plaintext.size()) {
		throw std::runtime_error("ERROR: digest_SHA256: Cannot digest an empty string");
	}

	if(!gcry_initialized) {
		init_gcrypt();
	}

	std::unique_ptr<unsigned char> buffer(new unsigned char[plaintext.size()]);
	memcpy((void*) buffer.get(), (void*) &plaintext[0], plaintext.size() + 1);

	size_t digest_size = gcry_md_get_algo_dlen(GCRY_MD_SHA512);
	std::unique_ptr<unsigned char> digest(new unsigned char[digest_size]);
	
	gcry_md_hash_buffer(GCRY_MD_SHA512, digest.get(), buffer.get(), plaintext.size());

	std::string res;
	for(size_t i = 0; i < digest_size; i++) {
		res.append(bzbstring::hex(digest.get()[i]));
	}

	return res;
}


std::string bzbcrypto::digest_SHA512(std::vector<unsigned char> data) {

	if(!data.size()) {
		throw std::runtime_error("ERROR: bzbcrypto: Cannot digest an empty vector");
	}

	if(!gcry_initialized) {
		init_gcrypt();
	}

	std::unique_ptr<unsigned char> buffer(new unsigned char[data.size()]);
	memcpy((void*) buffer.get(), (void*) &data[0], data.size() + 1);

	size_t digest_size = gcry_md_get_algo_dlen(GCRY_MD_SHA512);
	std::unique_ptr<unsigned char> digest(new unsigned char[digest_size]);
	
	gcry_md_hash_buffer(GCRY_MD_SHA512, digest.get(), buffer.get(), data.size());

	std::string res;
	for(size_t i = 0; i < digest_size; i++) {
		res.append(bzbstring::hex(digest.get()[i]));
	}

	return res;
}


#endif

